Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.4. 卷积神经网络(CNN)实战(进阶):使用VGG-16进行图像特征提取

本文承接 2.2. 卷积神经网络(CNN)模型理论(进阶):图像填充、LeNet、AlexNet,没看过的建议先看前文。

2.4.1. VGG-16的架构

在本文中我们会使用VGG-16进行图像特征提取,然后构建自己的二分类模型,我们先来回顾一下VGG-16模型的架构:

VGG16

层次说明输入尺寸输出尺寸过滤器步长填充激活
输入层RGB 图像$224 \times 224 \times 3$$224 \times 224 \times 3$----
Conv1-1$3 \times 3$ 卷积$224 \times 224 \times 3$$224 \times 224 \times 64$6411ReLU
Conv1-2$3 \times 3$ 卷积$224 \times 224 \times 64$$224 \times 224 \times 64$6411ReLU
Pool1最大池化$224 \times 224 \times 64$$112 \times 112 \times 64$$2 \times 2$20-
Conv2-1$3 \times 3$ 卷积$112 \times 112 \times 64$$112 \times 112 \times 128$12811ReLU
Conv2-2$3 \times 3$ 卷积$112 \times 112 \times 128$$112 \times 112 \times 128$12811ReLU
Pool2最大池化$112 \times 112 \times 128$$56 \times 56 \times 128$$2 \times 2$20-

(后续层类同)

特征提取后,我们会自己构建MLP模型,有一层隐藏层,10个神经元。

2.4.2. 准备工作

请在终端输入指令以下载和安装:

pip install keras tensorflow scikit-learn

我们的任务是对猫狗进行二分类,会使用到猫狗的照片,这里我们使用Kaggle数据集,各位可以到 kaggle数据集、mnist数据集、imdb数据集分享 这个帖子中找到下载链接,下载好后要先解压。在train文件夹中,创建cat和dog两个文件夹,把猫的照片放到cat中,把狗的照片放到dog中,最后的路径应该长这样:

.\train
	\train
		\dog
			\dog.0.jpg
			\dog.1.jpg
			\dog.2.jpg
			...
		\cat
			\cat.0.jpg
			\cat.1.jpg
			\cat.2.jpg
			...

把train文件夹直接拖入Python项目文件夹中即可。

如果你要在本地实操这个项目,强烈建议在Linux环境下使用带有CUDA加速单元的英伟达GPU,或是AMD GPU手动开启ROCm(仅限Linux)。你当然可以在Windows环境下运行,但是现在的tensorflow不支持Windows平台的GPU加速,所以只能靠纯CPU运算,计算速度会慢很多。我用i9-13900kf要10+分钟,用M1 Max要30分钟。

AMD GPU开启ROCm需要安装适配ROCm的TensorFlow版本(仅支持Linux),可以通过pip直接安装:

pip install tensorflow-rocm

运行以下代码检查TensorFlow是否能检测到AMD GPU:

import tensorflow as tf
print("Num GPUs Available:", len(tf.config.list_physical_devices('GPU')))

2.4.3. 节省内存版代码

Step 1: 初始化VGG-16

# 初始化 VGG 模型(去掉全连接层 MLP)
from tensorflow.keras.applications.vgg16 import VGG16
model = VGG16(weights='imagenet', include_top=False)
  • weights='imagenet' 表示加载的是基于 ImageNet 数据集预训练的权重
  • include_top=False:
    • 表示不加载 VGG-16 的顶层全连接层(即分类部分),只保留卷积部分
    • 这样处理的目的是将 VGG-16 作为一个特征提取器,提取高层次的卷积特征

Step 2: 加载图片

# 定义函数:加载与预处理单张图片
from tensorflow.keras.applications.vgg16 import preprocess_input
import numpy as np
from tensorflow.keras.preprocessing.image import img_to_array, load_img
def load_image_and_preprocess(image_path, model):
    """
    加载图片并进行预处理以适配 VGG 模型
    :param image_path: 图片路径
    :param model: VGG 模型实例
    :return: 预处理后的特征向量
    """
    img = load_img(image_path, target_size=(224, 224))  # 调整图片大小
    img = img_to_array(img)  # 转换为数组
    x = np.expand_dims(img, axis=0)  # 扩展维度以匹配模型输入格式
    x = preprocess_input(x)  # 应用预处理(均值中心化等处理)
    x_vgg = model.predict(x)  # 提取特征
    return np.reshape(x_vgg, (7 * 7 * 512))  # 展平特征

# 定义函数:处理单个文件夹的所有图片
import os
def process_folder(folder, model):
    """
    处理指定文件夹下的所有图片,提取特征向量
    :param folder: 图片文件夹路径
    :param model: VGG 模型实例
    :return: 提取的特征向量数组
    """
    # 获取所有 .jpg 文件路径
    img_paths = [
        os.path.join(folder, file)
        for file in os.listdir(folder)
        if file.endswith('.jpg')
    ]

    # 初始化特征数组(提取完特征之后的形状为 7*7*512)
    features = np.zeros((len(img_paths), 7 * 7 * 512))

    # 遍历所有文件路径,依次加载并提取特征
    for idx, path in enumerate(img_paths):
        features[idx] = load_image_and_preprocess(path, model)
        print(f"Preprocessed: {path}")

    return features, len(img_paths)

# 处理猫图片文件夹
cat_folder = "train/train/cat"
cat_features, cat_count = process_folder(cat_folder, model)  # 猫的特征和数量

# 处理狗图片文件夹
dog_folder = "train/train/dog"
dog_features, dog_count = process_folder(dog_folder, model)  # 狗的特征和数量

定义单张图片的加载与预处理函数 (load_image_and_preprocess):

  • 加载指定路径的图片,调整为 VGG16 需要的输入尺寸 224x224。
  • 将图片从 PIL 格式转换为 NumPy 数组,并扩展维度以匹配模型的输入格式(4D 张量)。
  • 使用 preprocess_input 对图片数据进行标准化(VGG16 需要特定格式的输入,如中心化操作)。
  • 调用 model.predict 使用 VGG16 模型提取特征,得到卷积层输出的特征图,大小为 (7, 7, 512)。
  • 展平特征图为一维向量(7×7×512),为后续处理做准备。

定义文件夹中所有图片的处理函数 (process_folder):

  • 遍历指定文件夹,筛选出所有以 .jpg 结尾的图片文件路径。
  • 初始化一个空的特征矩阵 (features),其大小为 (图片数量, 7×7×512)。
  • 对每张图片调用 load_image_and_preprocess,依次提取特征并存储到特征矩阵中。
  • 返回提取的特征矩阵以及图片数量。

提取猫和狗图片的特征:

  • 指定猫和狗图片存储的文件夹路径(cat_folder 和 dog_folder)。
  • 分别调用 process_folder 对这两个文件夹进行处理,提取每张图片的特征向量,返回特征矩阵(cat_features 和 dog_features)以及对应的图片数量(cat_count 和 dog_count)。

Step 3: 生成对应标签

我们需要为训练数据和测试数据提供对应的标签:

# 生成对应的标签(猫为 0,狗为 1)
cat_labels = np.zeros((cat_count, 1))
dog_labels = np.ones((dog_count, 1))

# 合并猫和狗的特征与标签
X = np.concatenate((cat_features, dog_features), axis=0)
y = np.concatenate((cat_labels, dog_labels), axis=0)

# 打印结果信息
print(f"Feature shape: {X.shape}, Label shape: {y.shape}")

输出:

Feature shape: (25000, 25088), Label shape: (25000, 1)

Step 4: 数据分离

我们需要把数据分为训练集和测试集,训练集用于训练模型,测试集用于测试模型的准确率:

# 数据分离  
from sklearn.model_selection import train_test_split  

X_train, X_test, y_train, y_test = train_test_split(X,
                                                    y,
                                                    test_size=0.2,
                                                    random_state=50)
  • X: 输入特征矩阵。这里的 X 是通过 VGG-16 提取并展平后的图片特征,形状为 (None, 7 * 7 * 512),用作训练和测试的特征集
  • y: 标签值,即图片所属的类别。这里沿用前文生成的标签,猫为 0,狗为 1
  • test_size=0.2: 表示测试集占整个数据集的比例。在此设置为 20%(即80%用作训练集,20%用作测试集)
  • random_state=50: 随机种子,用于确保数据分割的随机性一致。相同的随机种子保证每次运行程序时,分割出的训练集和测试集相同

Step 5: 建立MLP模型

接下来我们建立自己的MLP模型,有一层隐藏层,10个神经元,输出层有1个神经元表示二分类:

# MLP模型  
from keras.models import Sequential  
from keras.layers import Dense  
model = Sequential()  
model.add(Dense(units=10, activation='relu', input_dim=7 * 7 * 512))  
model.add(Dense(units=1, activation='sigmoid'))

Step 6: 配置模型

# 配置模型  
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
  • compile 是 Keras 模型的方法,用于在训练模型之前配置模型的学习过程,它的主要参数有:

    • 损失函数(loss):定义模型训练时计算误差的方法。
    • 优化器(optimizer):决定模型的参数(如权重和偏置)如何更新,以最小化损失函数。
    • 评估指标(metrics):决定用于评估模型性能的指标,这些指标不会影响模型训练,只用于衡量效果。
  • loss='binary_crossentropy':binary_crossentropy(二元交叉熵)是二分类任务中常用的损失函数

  • optimizer='adam':adam(Adaptive Moment Estimation)是一种自适应学习率优化算法,是深度学习中最常用的优化器之一

  • metrics=['accuracy']:指定模型在训练和评估时用准确率来衡量性能

Step 7: 训练模型

把训练集数据喂给模型即可:

# 训练模型  
model.fit(X_train, y_train, epochs=50)

Step 8: 评估模型

我们要分别测试训练集和测试集准确率:

# 训练集准确率  
train_accuracy = model.evaluate(X_train, y_train)[1]  
print('Train Accuracy: ', train_accuracy)  
  
# 测试集准确率  
test_accuracy = model.evaluate(X_test, y_test)[1]  
print('Test Accuracy: ', test_accuracy)

输出:

Train Accuracy:  0.9991000294685364
Test Accuracy:  0.9833999872207642

效果非常好!

2.4.4. 效率更高版代码

上文中我们将照片一个个处理,这样写的好处在于不占内存,但是效率较低。我们也可以一次性把所有照片加载进内存里一次性处理完,这样写效率更高。

实测,如果采用一次性把所有照片加载进内存的办法将会占用20GB内存,加上IDE(我用的是PyCharm Professional)一共要占23GB内存,也就是说32GB内存才能勉强跑,48GB才能保证不会爆内存。

如果你的电脑内存够大,你可以尝试以下这段代码:

import os  
import numpy as np  
from tensorflow.keras.preprocessing.image import load_img, img_to_array  
from tensorflow.keras.applications.vgg16 import VGG16, preprocess_input  
  
# 初始化 VGG 模型  
model = VGG16(weights='imagenet', include_top=False)  
  
  
def load_and_preprocess_images(folder):  
    """  
    批量加载并预处理指定文件夹中的所有图片  
    :param folder: 图片文件夹路径  
    :return: 预处理后的图像批量数据和图像数量  
    """  
    # 检查路径是否存在  
    if not os.path.exists(folder):  
        raise FileNotFoundError(f"文件夹不存在:{folder}")  
  
    # 获取所有 .jpg 文件路径  
    img_paths = [  
        os.path.join(folder, file)  
        for file in os.listdir(folder)  
        if file.endswith('.jpg')  
    ]  
  
    if not img_paths:  
        raise ValueError(f"文件夹中未找到图片文件:{folder}")  
  
    # 批量加载所有图片并调整大小为 (224, 224)  
    images = np.array([  
        img_to_array(load_img(img_path, target_size=(224, 224)))  
        for img_path in img_paths  
    ])  
  
    # 应用预处理(如均值中心化)  
    images = preprocess_input(images)  
  
    print(f"Loaded {len(images)} images from {folder}")  
    return images, len(img_paths)  
  
  
def process_folder_in_batch(folder, model):  
    """  
    批量处理指定文件夹中的所有图片,提取特征向量  
    :param folder: 图片文件夹路径  
    :param model: VGG 模型实例  
    :return: 提取的特征向量数组和图片数量  
    """  
    # 加载并预处理所有图片  
    images, img_count = load_and_preprocess_images(folder)  
  
    # 批量提取特征向量  
    features = model.predict(images)  
    features = features.reshape(img_count, -1)  # 展平成 (N, 7*7*512) 的特征矩阵  
  
    print(f"Extracted features for {img_count} images from {folder}")  
    return features, img_count  
  
# 路径  
BASE_DIR = os.path.abspath("train/train")  
cat_folder = os.path.join(BASE_DIR, "cat")  
dog_folder = os.path.join(BASE_DIR, "dog")  
  
# 处理猫图片文件夹  
try:  
    cat_features, cat_count = process_folder_in_batch(cat_folder, model)  
except Exception as e:  
    print(f"处理猫图片文件夹时发生错误: {e}")  
  
# 处理狗图片文件夹  
try:  
    dog_features, dog_count = process_folder_in_batch(dog_folder, model)  
except Exception as e:  
    print(f"处理狗图片文件夹时发生错误: {e}")  
  
# 检查是否成功提取特征  
if 'cat_features' in locals() and 'dog_features' in locals():  
    # 生成对应的标签(猫为 0,狗为 1)  
    cat_labels = np.zeros((cat_count, 1))  
    dog_labels = np.ones((dog_count, 1))  
  
    # 合并特征和标签  
    X = np.concatenate((cat_features, dog_features), axis=0)  
    y = np.concatenate((cat_labels, dog_labels), axis=0)  
  
    print(f"Feature shape: {X.shape}, Label shape: {y.shape}")  
  
    # 数据分离  
    from sklearn.model_selection import train_test_split  
  
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=50)  
else:  
    print("特征提取失败,无法继续后续处理。")  
  
# 建立MLP模型  
from tensorflow.keras.models import Sequential  
from tensorflow.keras.layers import Dense  
model = Sequential()  
model.add(Dense(units=10, activation='relu', input_dim = 7 * 7 * 512))  
model.add(Dense(units=1, activation='sigmoid'))  
  
# 配置模型  
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])  
  
# 训练模型  
model.fit(X_train, y_train, epochs=50)  
  
# 训练集准确率  
train_accuracy = model.evaluate(X_train, y_train)[1]  
print('Train Accuracy: ', train_accuracy)  
  
# 测试集准确率  
test_accuracy = model.evaluate(X_test, y_test)[1]  
print('Test Accuracy: ', test_accuracy)